/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.jena.rdf.model.test;

import junit.framework.*;
import org.apache.jena.rdf.model.*;
import org.apache.jena.shared.Lock;

public class TestConcurrency extends TestSuite {

    /** Creates new RDQLTestSuite */
    static public TestSuite suite() {
        return new TestConcurrency();
    }

    // Test suite to exercise the locking
    static long SLEEP = 100;
    static int threadCount = 0;

    // Note : reuse the model across tests.
    final static Model model1 = ModelFactory.createDefaultModel();
    final static Model model2 = ModelFactory.createDefaultModel();

    public TestConcurrency() {
        super("Model concurrency control");

        if ( true ) {
            // Same model: inner and outer
            addTest(new Nesting("Lock nesting 1 - same model", model1, Lock.READ, Lock.READ, false));
            addTest(new Nesting("Lock nesting 2 - same model", model1, Lock.WRITE, Lock.WRITE, false));
            addTest(new Nesting("Lock nesting 3 - same model", model1, Lock.READ, Lock.WRITE, true));
            addTest(new Nesting("Lock nesting 4 - same model", model1, Lock.WRITE, Lock.READ, false));

            // Different model: inner and outer
            addTest(new Nesting("Lock nesting 1 - different models", model1, Lock.READ, model2, Lock.READ, false));
            addTest(new Nesting("Lock nesting 2 - different models", model1, Lock.WRITE, model2, Lock.WRITE, false));
            addTest(new Nesting("Lock nesting 3 - different models", model1, Lock.READ, model2, Lock.WRITE, false));
            addTest(new Nesting("Lock nesting 4 - different models", model1, Lock.WRITE, model2, Lock.READ, false));
        }
        if ( true ) {
            // Crude test
            addTest(new Parallel("Parallel concurrency test"));
        }

    }

    static class Nesting extends TestCase {
        Model outerModel;
        Model innerModel;
        boolean outerLock;
        boolean innerLock;
        boolean exceptionExpected;

        // Same model
        Nesting(String testName, Model model, boolean lock1, boolean lock2, boolean exExpected) {
            this(testName, model, lock1, model, lock2, exExpected);
        }

        // Potetnially different models
        Nesting(String testName, Model model1, boolean lock1, Model model2, boolean lock2, boolean exExpected) {
            super(testName);
            outerModel = model1;
            outerLock = lock1;
            innerModel = model2;
            innerLock = lock2;
            exceptionExpected = exExpected;
        }

        @Override
        protected void runTest() {
            boolean gotException = false;
            try {
                outerModel.enterCriticalSection(outerLock);

                try {
                    try {
                        // Should fail if outerLock is READ and innerLock is WRITE
                        // and its on the same model, inner and outer.
                        innerModel.enterCriticalSection(innerLock);

                    } finally {
                        innerModel.leaveCriticalSection();
                    }
                } catch (Exception ex) {
                    gotException = true;
                }

            } finally {
                outerModel.leaveCriticalSection();
            }

            if ( exceptionExpected )
                assertTrue("Failed to get expected lock promotion error", gotException);
            else
                assertTrue("Got unexpected lock promotion error", !gotException);
        }
    }

    static class Parallel extends TestCase {
        int threadTotal = 10;

        Parallel(String testName) {
            super(testName);
        }

        @Override
        protected void runTest() {
            Model model = ModelFactory.createDefaultModel();
            Thread threads[] = new Thread[threadTotal];

            boolean getReadLock = Lock.READ;
            for ( int i = 0 ; i < threadTotal ; i++ ) {
                String nextId = "T" + Integer.toString(++threadCount);
                threads[i] = new Operation(model, getReadLock);
                threads[i].setName(nextId);
                threads[i].start();

                getReadLock = !getReadLock;
            }

            boolean problems = false;
            for ( int i = 0 ; i < threadTotal ; i++ ) {
                try {
                    threads[i].join(200 * SLEEP);
                } catch (InterruptedException intEx) {}
            }

            // Try again for any we missed.
            for ( int i = 0 ; i < threadTotal ; i++ ) {
                if ( threads[i].isAlive() )
                    try {
                        threads[i].join(200 * SLEEP);
                    } catch (InterruptedException intEx) {}
                if ( threads[i].isAlive() ) {
                    System.out.println("Thread " + threads[i].getName() + " failed to finish");
                    problems = true;
                }
            }

            assertTrue("Some thread failed to finish", !problems);
        }

        class Operation extends Thread {
            Model model;
            boolean readLock;

            Operation(Model m, boolean withReadLock) {
                model = m;
                readLock = withReadLock;
            }

            @Override
            public void run() {
                for ( int i = 0 ; i < 2 ; i++ ) {
                    try {
                        model.enterCriticalSection(readLock);
                        if ( readLock )
                            readOperation(false);
                        else
                            writeOperation(false);
                    } finally {
                        model.leaveCriticalSection();
                    }
                }
            }
        }
        // Operations ----------------------------------------------

        volatile int writers = 0;

        // The example model operations
        void doStuff(String label, boolean doThrow) {
            String id = Thread.currentThread().getName();
            // Puase a while to cause other threads to (try to) enter the region.
            try {
                Thread.sleep(SLEEP);
            } catch (InterruptedException intEx) {}
            if ( doThrow )
                throw new RuntimeException(label);
        }

        // Example operations

        public void readOperation(boolean doThrow) {
            if ( writers > 0 )
                System.err.println("Concurrency error: writers around!");
            doStuff("read operation", false);
            if ( writers > 0 )
                System.err.println("Concurrency error: writers around!");
        }

        public void writeOperation(boolean doThrow) {
            writers++;
            doStuff("write operation", false);
            writers--;

        }
    }
}
